package vulns

import (
	"slices"
	"sort"
	"strings"

	"github.com/google/osv-scalibr/inventory/osvecosystem"
	"github.com/google/osv-scalibr/semantic"
	"github.com/google/osv-scanner/v2/internal/cmdlogger"
	"github.com/google/osv-scanner/v2/internal/imodels"
	"github.com/ossf/osv-schema/bindings/go/osvschema"
)

func eventVersion(e *osvschema.Event) string {
	if e.GetIntroduced() != "" {
		return e.GetIntroduced()
	}

	if e.GetFixed() != "" {
		return e.GetFixed()
	}

	if e.GetLimit() != "" {
		return e.GetLimit()
	}

	if e.GetLastAffected() != "" {
		return e.GetLastAffected()
	}

	return ""
}

func rangeContainsVersion(ar *osvschema.Range, pkg imodels.PackageInfo) bool {
	if ar.GetType() != osvschema.Range_ECOSYSTEM && ar.GetType() != osvschema.Range_SEMVER {
		return false
	}
	// todo: we should probably warn here
	if len(ar.GetEvents()) == 0 {
		return false
	}

	vp := semantic.MustParse(pkg.Version(), string(pkg.Ecosystem().Ecosystem))

	sort.Slice(ar.GetEvents(), func(i, j int) bool {
		a := ar.GetEvents()[i]
		b := ar.GetEvents()[j]

		if a.GetIntroduced() == "0" {
			return true
		}

		if b.GetIntroduced() == "0" {
			return false
		}

		// Ignore errors as we assume the version is correct
		order, _ := semantic.MustParse(eventVersion(a), string(pkg.Ecosystem().Ecosystem)).CompareStr((eventVersion(b)))

		return order < 0
	})

	var affected bool
	for _, e := range ar.GetEvents() {
		if affected {
			if e.GetFixed() != "" {
				order, _ := vp.CompareStr(e.GetFixed())
				affected = order < 0
			} else if e.GetLastAffected() != "" {
				order, _ := vp.CompareStr(e.GetLastAffected())
				affected = e.GetLastAffected() == pkg.Version() || order <= 0
			}
		} else if e.GetIntroduced() != "" {
			order, _ := vp.CompareStr(e.GetIntroduced())
			affected = e.GetIntroduced() == "0" || order >= 0
		}
	}

	return affected
}

// rangeAffectsVersion checks if the given version is within the range
// specified by the events of any "Ecosystem" or "Semver" type ranges
func rangeAffectsVersion(a []*osvschema.Range, pkg imodels.PackageInfo) bool {
	for _, r := range a {
		if r.GetType() != osvschema.Range_ECOSYSTEM && r.GetType() != osvschema.Range_SEMVER {
			return false
		}
		if rangeContainsVersion(r, pkg) {
			return true
		}
	}

	return false
}

func AffectsEcosystem(v *osvschema.Vulnerability, ecosystemAffected osvecosystem.Parsed) bool {
	for _, affected := range v.GetAffected() {
		if osvecosystem.MustParse(affected.GetPackage().GetEcosystem()).Equal(ecosystemAffected) {
			return true
		}
	}

	return false
}

// NormalizeRepo applies some reasonable transformations to repository urls to
// ensure accurate results when determining if two repository urls are referencing
// the same repository.
//
// Specifically, common protocols are removed from the start of the url and the
// ".git" suffix if present
func NormalizeRepo(repo string) string {
	repo = strings.TrimPrefix(repo, "https://")
	repo = strings.TrimPrefix(repo, "http://")
	repo = strings.TrimPrefix(repo, "git://")

	return strings.TrimSuffix(repo, ".git")
}

func hasGitRangeForRepo(affected *osvschema.Affected, repo string) bool {
	for _, r := range affected.GetRanges() {
		if r.GetType() == osvschema.Range_GIT && NormalizeRepo(r.GetRepo()) == NormalizeRepo(repo) {
			return true
		}
	}

	return false
}

func IsAffected(v *osvschema.Vulnerability, pkg imodels.PackageInfo) bool {
	for _, affected := range v.GetAffected() {
		// assume we're dealing with a git-source package whose name is the git repository, and that the version is the tag
		// the underlying commit has been resolved to (somehow), meaning we can check if it's in the versions listed by the advisory
		if pkg.Ecosystem().IsEmpty() && pkg.Commit() != "" && pkg.Version() != "" {
			if hasGitRangeForRepo(affected, pkg.Name()) && slices.Contains(affected.GetVersions(), pkg.Version()) {
				return true
			}
		}

		// Assume vulnerability has already been validated
		if affected.GetPackage() == nil {
			continue
		}
		if osvecosystem.MustParse(affected.GetPackage().GetEcosystem()).Equal(pkg.Ecosystem()) &&
			affected.GetPackage().GetName() == pkg.Name() {
			if len(affected.GetRanges()) == 0 && len(affected.GetVersions()) == 0 {
				cmdlogger.Warnf("%s does not have any ranges or versions - this is probably a mistake!", v.GetId())

				continue
			}

			if slices.Contains(affected.GetVersions(), pkg.Version()) {
				return true
			}

			if rangeAffectsVersion(affected.GetRanges(), pkg) {
				return true
			}

			// if a package does not have a version, assume it is vulnerable
			// as false positives are better than false negatives here
			if pkg.Version() == "" {
				return true
			}
		}
	}

	return false
}

// PackageKey uniquely identifies a package in a vulnerability.
type PackageKey struct {
	Name      string
	Ecosystem string
	Purl      string
}

// NewPackageKey creates a PackageKey from osvschema.Package.
func NewPackageKey(pkg *osvschema.Package) PackageKey {
	return PackageKey{
		Name:      pkg.GetName(),
		Ecosystem: pkg.GetEcosystem(),
		Purl:      pkg.GetPurl(),
	}
}

// GetFixedVersions returns a map of fixed versions for each package, or a map of empty slices if no fixed versions are available
func GetFixedVersions(v *osvschema.Vulnerability) map[PackageKey][]string {
	output := map[PackageKey][]string{}
	for _, a := range v.GetAffected() {
		if a.GetPackage() == nil {
			continue
		}
		packageKey := NewPackageKey(a.GetPackage())
		packageKey.Purl = ""
		for _, r := range a.GetRanges() {
			for _, e := range r.GetEvents() {
				if e.GetFixed() != "" {
					output[packageKey] = append(output[packageKey], e.GetFixed())
					if strings.Contains(packageKey.Ecosystem, ":") {
						unversionedKey := packageKey
						unversionedKey.Ecosystem = strings.Split(packageKey.Ecosystem, ":")[0]
						output[unversionedKey] = append(output[unversionedKey], e.GetFixed())
					}
				}
			}
		}
	}

	return output
}
